﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using Business.Entities;
using Business.Logic;
using Microsoft.WindowsMobile.PocketOutlook;
using Task=Business.Entities.Task;
using TaskItem=Microsoft.WindowsMobile.PocketOutlook.Task;
using Business;

namespace GogTasks.Entities
{
    public class MobileOutlookApi : IOutlookApi
    {
        #region IOutlookApi Members
        public string ErrorLog;
        /// <summary>
        /// Tasks with corrupted timestamp
        /// </summary>
        private List<Task> CorruptedTasks = new List<Task>();

        /// <summary>
        /// dd.MM.yyyy HH:mm:ss
        /// </summary>
        private const string DateFormat = "dd.MM.yyyy HH:mm:ss";
        /// <summary>
        /// For some reason, I get error logs where this log format is stored. Use as fallback when parsing
        /// </summary>
        private const string DateFormatFallback = "dd.MM.yyyy HH.mm.ss";
        private static string GoogleTaskId = "GoogleTaskId";
        private static string TaskSnapshot = "TaskSnapshot";
        private static string LastModfied = "LastModfied";
        private static string OrdinalProperty = "Ordinal";
        private static string ListIdProperty = "ListId";
        //private static string IndentLevel = "IndentLevel";
        private static string ChildIds = "ChildIds";
        private OutlookSession _outlook;

        /// <summary>
        ///  cache / dictionary
        /// </summary>
        private Dictionary<string, TaskItem> _googleIdTaskMap;

        public MobileOutlookApi()
        {
            _outlook = new OutlookSession();
        }

        public OutlookSession GetSession()
        {
            return _outlook;
        }

        public void SetGoogleId(Task googleTask)
        {
            SetGoogleId(googleTask, false);
        }

        public void SetGoogleId(Task googleTask, bool updateListId)
        {
            if (googleTask.id == null) return;
            var oTask = (TaskItem)googleTask.OutlookTask;
            //if (!oTask.Properties.Contains(GoogleTaskId)) oTask.Properties.Add(GoogleTaskId);
            //oTask.Properties[GoogleTaskId] = googleTask.id;
            SetProperty(oTask, GoogleTaskId, googleTask.id);
            if (updateListId) SetProperty(oTask, ListIdProperty, googleTask.ListId);
            oTask.Update();
        }

        public Task ConvertTask(object _oTask)
        {
            var oTask = (TaskItem) _oTask;
            Task gTask = new Task();
            gTask.name = oTask.Subject;
            gTask.notes = oTask.Body;
            gTask.TaskDate = oTask.DueDate;
            gTask.completed = oTask.Complete;
            gTask.OutlookId = oTask.ItemId.ToString();
            gTask.OutlookTask = _oTask;
            
            var id = GetProperty(oTask, GoogleTaskId);
            if(!String.IsNullOrEmpty(id)) gTask.id = id;
            gTask.Ordinal = GetPropertyInt(oTask, OrdinalProperty);
            //gTask.IndentLevel = GetPropertyInt(oTask, IndentLevel);
            gTask.child_id = GetPropertyArray(oTask, ChildIds);
            gTask.ListId = GetProperty(oTask, ListIdProperty);

            // Find out last modified. 
            var lastModString = GetProperty(oTask, LastModfied);
            if (lastModString != null)
            {
                bool ok = SetTaskDateFromString(gTask, lastModString);
                SetModifiedFromSnapshot(gTask, !ok);
                if (!ok) CorruptedTasks.Add(gTask);
            }
            else SetModifiedFromSnapshot(gTask, true);

            return gTask;
        }

        private bool SetTaskDateFromString(Task gTask, string lastModString)
        {
            try
            {
                var utcDateStored = DateTime.ParseExact(lastModString, DateFormat, new DateTimeFormatInfo());
                gTask.LastModified = utcDateStored.ToLocalTime();
                return true;
            }
            catch (FormatException)
            {
                try // Fallback. 
                {
                    var utcDateStored = DateTime.ParseExact(lastModString, DateFormatFallback, new DateTimeFormatInfo());
                    gTask.LastModified = utcDateStored.ToLocalTime();
                    return true;
                }
                catch (FormatException)
                {
                    ErrorLog += "ParseError: [" + lastModString + "] in format " + DateFormat + Environment.NewLine;
                    return false;
                }
            }
        }

        private string[] GetPropertyArray(TaskItem oTask, string propertyName)
        {
            string data = GetProperty(oTask, propertyName);
            if (data == null) return null;
            if (data == String.Empty) return null;
            return data.Split(';');
        }

        /// <summary>
        ///  Updates last modified
        /// </summary>
        public void Touch(Task gTask)
        {
            Touch(gTask, DateTime.Now);
        }

        /// <summary>
        ///  Updates last modified
        /// </summary>
        public void Touch(Task gTask, DateTime timeStamp)
        {
            var oTask = TouchNoUpdate(gTask, timeStamp);
            oTask.Update();
        }
        
        /// <summary>
        ///  Updates last modified without saving otask
        /// </summary>
        private TaskItem TouchNoUpdate(Task gTask, DateTime timeStamp)
        {
            var oTask = (TaskItem)gTask.OutlookTask;
            var utcTime = timeStamp.ToUniversalTime();
            var dateAsString = utcTime.ToString(DateFormat);
            gTask.LastModified = utcTime;
            SetProperty(oTask, LastModfied, dateAsString);
            return oTask;
        }

        /// <summary>
        /// Finds out if the outlook task has been modified since last sync
        /// </summary>
        /// <param name="gTask"></param>
        private void SetModifiedFromSnapshot(Task gTask, bool setFromGoogleTask)
        {
            TaskItem oTask = (TaskItem) gTask.OutlookTask;
            String snapshot = GetProperty(oTask, TaskSnapshot);
            if (!string.IsNullOrEmpty(snapshot))
            {
                Task oldTask = Context.JsonApi.Deserialize<Task>(snapshot);
                if (setFromGoogleTask) gTask.LastModified = oldTask.LastModified;
                var equals = gTask.DataEquals(oldTask);
                if (!equals)
                {
                    // modification made. Pump up the mod.time so that
                    // outlook changes win over google changes?
                    var z = TimeZone.CurrentTimeZone;
                    if (MobileContext.Config.OutlookWinsSyncConflict) 
                        gTask.LastModified = DateTime.Now;
                }
                //Trace.WriteLine("  SNAP " + gTask + "; EQ="+equals+", LM=" + gTask.LastModified.ToString("HH:mm:ss"));
            }
        }


        /// <summary>
        /// Takes data from Task  and copies to Task.OutlookTask and saves to outlook
        /// </summary>
        /// <param name="task">Task item</param>
        /// <param name="category">Category (task list name)</param>
        public void InsertTask(Task task, string category)
        {
            task.OutlookTask = _outlook.Tasks.Items.AddNew();
            if (task.OutlookTask == null) throw new System.Exception("Error creating new outlook task '" + task.name + "'");

            UpdateTask(task, category, false);
        }

        public void UpdateTask(Task task, string category)
        {
            UpdateTask(task, category, false);
        }

        /// <summary>
        /// Takes data from Task  and copies to Task.OutlookTask and saves to outlook
        /// </summary>
        /// <param name="task">Task item</param>
        /// <param name="updateTimestamp">Update time stamp aswell?</param>
        /// <param name="category">Category (task list name)</param>
        public void UpdateTask(Task task, string category, bool updateTimestamp)
        {
            // find or create element
            if (task.OutlookTask == null)
            {
                task.OutlookTask = GetItemByGoogleId(task.id);
                if (task.OutlookTask == null) throw new ArgumentException("Attempt to update a non-existing Outlook task from Google: {" + task + "}");
            }
            SetTaskDataAndSave(task, category, updateTimestamp);
        }


        public void InsertOrUpdateTask_(Task task, string category)
        {
            InsertOrUpdateTask_(task, category, false);
        }

        /// <summary>
        /// Updates or replaces existing outlook task. 
        /// </summary>
        /// <param name="task">Task object with OutlookTask object set</param>
        /// <param name="category">Category or null of no change on an update</param>
        /// <param name="updateTimestamp">True to update timestamp</param>
        public void SetTaskDataAndSave(Task task, string category, bool updateTimestamp)
        {
            var oTask = (TaskItem)task.OutlookTask;
            if (oTask == null) throw new System.ArgumentException("Empty task item");
            // Set data and save
            oTask.Subject = task.name;
            oTask.Body = task.notes;
            oTask.Complete = task.completed;
            if (task.TaskDate != DateTime.MinValue) oTask.DueDate = task.TaskDate;
            else oTask.DueDate = OutlookNullDate;
            // Do not copy the ordinal. Ordering is done in a separate list-scope action
            //oTask.Ordinal = task.Ordinal; 
            if (category != null) oTask.Categories = category.Replace(",", "¤¤").Replace(";", "~~");  // Semicolon and Comma are forbidden in POOM
            SetGoogleId(task, false);
            SetSnapshot(task, false);
            SetProperty(oTask, OrdinalProperty, task.Ordinal.ToString());
            SetPropertyArray(oTask, ChildIds, task.child_id);
            SetProperty(oTask, ListIdProperty, task.ListId);
            TouchNoUpdate(task, DateTime.Now);
            oTask.Update();
        }


        
        /// <summary>
        /// Updates or replaces existing outlook task. 
        /// </summary>
        /// <param name="task">Task object with OutlookTask object set</param>
        /// <param name="category">Category or null of no change on an update</param>
        /// <param name="updateTimestamp">True to update timestamp</param>
        private void InsertOrUpdateTask_(Task task, string category, bool updateTimestamp)
        {
            // find or create element
            if (task.OutlookTask == null) task.OutlookTask = GetItemByGoogleId(task.id);
            if (task.OutlookTask == null)
            {
                task.OutlookTask = _outlook.Tasks.Items.AddNew();
            }
            //task.LastModified = DateTime.Now;
            var oTask = (TaskItem) task.OutlookTask;
            if (oTask == null) throw new System.Exception("Error creating new outlook task '" + task.name + "'");
            // Set data and save
            oTask.Subject = task.name;
            oTask.Body = task.notes;
            oTask.Complete = task.completed;
            if (task.TaskDate != DateTime.MinValue) oTask.DueDate = task.TaskDate;
            else oTask.DueDate = OutlookNullDate;
            // Do not copy the ordinal. Ordering is done in a separate list-scope action
            //oTask.Ordinal = task.Ordinal; 
            if (category != null) oTask.Categories = category.Replace(",", "¤¤").Replace(";", "~~");  // Semicolon and Comma are forbidden in POOM
            SetGoogleId(task, false);
            SetSnapshot(task, false);
            SetProperty(oTask, OrdinalProperty, task.Ordinal.ToString());
            SetPropertyArray(oTask, ChildIds, task.child_id);
            SetProperty(oTask, ListIdProperty, task.ListId);
            TouchNoUpdate(task, DateTime.Now);
            oTask.Update();
        }

        private void SetPropertyArray(TaskItem oTask, string propertyName, string[] values)
        {
            if (values == null) SetProperty(oTask, propertyName, string.Empty);
            else
            {
                string data = String.Join(";", values);
                SetProperty(oTask, propertyName, data);
            }
        }

        /// <summary>
        /// Stores a snapshot of all properties to a user property. 
        /// Needed to know if changes are done, since LastModified is not a property 
        /// in outlook mobile
        /// </summary>
        /// <param name="save">True to save immedieately</param>
        /// <param name="task"></param>
        public void SetSnapshot(Task task, bool save)
        {
            var oTask = (TaskItem) task.OutlookTask;
            var taskAsString = Context.JsonApi.Serialize(task);
            SetProperty(oTask, TaskSnapshot, taskAsString);
            if (save) oTask.Update();
        }

        private string GetProperty(TaskItem task,string propertyName)
        {
            String val = null;
            if (task.Properties.Contains(propertyName))
                val = task.Properties[propertyName] as String;
            return val;
        }
        private int GetPropertyInt(TaskItem task, string propertyName)
        {
            int val = 0;
            string sVal = GetProperty(task, propertyName);
            if (sVal != null) StringUtil.TryParseInt(sVal, ref val);
            return val;
        }

        private void SetProperty(TaskItem task, string propertyName, string value)
        {
            if (value == null) value = String.Empty;
            if (!task.Properties.Contains(propertyName))
            {
                task.Properties.Add(propertyName);
            }
            task.Properties[propertyName] = value;
        }

        private DateTime OutlookNullDate = new DateTime(4501, 01, 01); 

        public void DeleteTask(object oTask)
        {
            var task = (TaskItem) oTask;
            task.Delete();
        }

        public void SetOrdinalByGoogleId(string googleId, int ordinal, string[] child_id)
        {
            var task = GetItemByGoogleId(googleId);
            if (task == null) throw new ArgumentException("Could not find taskItem with id " + googleId);
            SetProperty(task, OrdinalProperty, ordinal.ToString());
            SetPropertyArray(task, ChildIds, child_id);
            task.Update();
        }

        public void SetOrdinalByCachedGoogleId(string googleId, int ordinal, string[] child_id)
        {
            var task = GetItemByCachedGoogleId(googleId);
            if (task == null) throw new ArgumentException("Could not find taskItem with id " + googleId);
            SetProperty(task, OrdinalProperty, ordinal.ToString());
            SetPropertyArray(task, ChildIds, child_id);
            task.Update();
        }

        private TaskItem GetItemByCachedGoogleId(string googleId)
        {
            if (_googleIdTaskMap == null) throw new ArgumentException("Google Id to outlook task cache not initialized");
            TaskItem task = null;
            _googleIdTaskMap.TryGetValue(googleId, out task);
            return task;
        }



        public TasksRoot GetOutlookTasks()
        {
            CorruptedTasks = new List<Task>();
            var root = new TasksRoot();
            _googleIdTaskMap = new Dictionary<string, TaskItem>();
            foreach (TaskItem oTask in _outlook.Tasks.Items)
            {
                Task task = ConvertTask(oTask);
                if (task.GoogleId != null) _googleIdTaskMap.Add(task.GoogleId, oTask);
                string listName = GetFirstCategory(oTask.Categories);
                TaskList list = root.GetListByName(listName);
                if (list == null)
                {
                    // Create new
                    list = new TaskList(listName);
                    if (!String.IsNullOrEmpty(task.ListId)) list.id = task.ListId;
                    else task.ListId = list.id; 
                    root.lists.Add(list);
                    //Trace.WriteLine("List: " + list + "," + list.id) ;
                }
                list.Add(task);
                if (!list.HasGoogleId && !String.IsNullOrEmpty(task.ListId)) list.SetNewId(task.ListId); // If first in list has wrong id, update at later point
            }
            // Post proccess
            root.PostProcess();
            // Trace
            //foreach (Task task in root.GetAllTasks())
            //{
            //    Trace.WriteLine("Task: " + task + "," + task.id + ",List: " + task.ListId + ", parent:" + task.ParentElementId);
            //}

            FixCorruptedTasks();

            return root;
        }

        private void FixCorruptedTasks()
        {
            foreach (Task task in CorruptedTasks)
            {
                Touch(task, DateTime.MinValue.AddDays(1));
            }
            CorruptedTasks.Clear();
        }

        private string GetFirstCategory(string p)
        {
            string[] list = p.Split(',');
            return list[0].Replace("¤¤",",").Replace("~~", ";").Trim();
        }

        public bool ExistsGoogleTask(string googleId)
        {
            var task = GetItemByGoogleId(googleId);
            return task != null;
        }

        public Task GetGoogleTask(string googleId)
        {
            var task = GetItemByGoogleId(googleId);
            if (task == null) return null;
            return ConvertTask(task);
        }

        public List<string> GetGoogleIds()
        {
            var ids = new List<string>();
            foreach (TaskItem elem in _outlook.Tasks.Items)
            {
                string gId = GetProperty(elem, GoogleTaskId);
                if (!string.IsNullOrEmpty(gId) && !ids.Contains(gId)) ids.Add(gId);
            }
            return ids;
        }

        #endregion

        public TaskItem GetItemByGoogleId(string googleId)
        {
            //using (var outlook = new OutlookSession())
            //{
            TaskItem found = null;
            foreach (TaskItem oTask in _outlook.Tasks.Items)
            {
                if (oTask.Properties.Contains(GoogleTaskId))
                {
                    string gId = (string) oTask.Properties[GoogleTaskId];
                    if (gId == googleId) found = oTask; // Do not break due to bug in framework causing exception
                }
            }
            return found;
                //_outlook.Tasks.Items.GetEnumerator().Reset();
            //}
        }
    }
}
